'use strict';

import Tool from './utils/tool'
import { Context } from 'egg'
import { uuid } from '../../utils/regular'

// redis 存储用户数据的键值
const WANG_ADMIN_USER_KEY = 'WANG_ADMIN_USER_KEY'
// redis 存储用户 token 的键值
const WANG_ADMIN_USER_TOKEN_KEY = 'WANG_ADMIN_USER_TOKEN_KEY'

export default {

  /**
   * 通过用户 ID 获取缓存的用户数据
   */
  async getUserData() {
    const ctx = this as Context
    if (ctx.userData) {
      return ctx.userData
    }
    const userId = this.getUserId()
    const { redis } = ctx.app as any
    const userData = await redis.get(`${WANG_ADMIN_USER_KEY}_${userId}`);
    if (userData) {
      return JSON.parse(userData)
    } else {
      const { User } = ctx.model as any;
      // 查找账号
      return await User.findWithID(userId);
    }
  },

  /**
   * 设置用户数据（redis）
   */
  async setReidsUserDataWithUser(userData) {
    const ctx = this as Context 
    const { redis, config  } = ctx.app as any
    await redis.set(`${WANG_ADMIN_USER_KEY}_${userData._id}`, JSON.stringify(userData), 'EX', config.token.validTime * 1000);
  },

  /**
   * 设置用户数据（redis）
   */
  async clearRedisUserData(userId: string) {
    const ctx = this as Context 
    const { redis  } = ctx.app as any
    await redis.set(`${WANG_ADMIN_USER_KEY}_${userId}`, '', 'EX', 1);
  },

  /**
   * 获取token
   */
  getToken(): string {
    const ctx = this as Context
    // 首先获取 token
    return ctx.req.headers.token as string
  },

  /**
   * 获取 taken 校验的数据
   */
  getVerifyTokenData(): any {
    const ctx = this as Context
    if (ctx.verifyToken) {
      return ctx.verifyToken
    }
    const { jwt, config } = ctx.app
    const token = this.getToken()
    const data = jwt.verify(token, config.jwt.secret);
    ctx.verifyToken = data
    return data
  },

  /**
   * 获取缓存的 token
   */
  async getCatchToken(id?: string) {
    const ctx = this as Context
    const { redis  } = ctx.app as any
    const userId = id ? id : this.getUserId()
    return await redis.get(`${WANG_ADMIN_USER_TOKEN_KEY}_${userId}`);
  },

  /**
   * 根据 token 获取用户ID
   */
  getUserId(): string {
    return this.getVerifyTokenData().userId
  },

  /**
   * 设置token
   */
  async createToken(userId: string) {
    const ctx = this as Context
    const { jwt, config, redis } = ctx.app as any
    // this 就是 app 对象，在其中可以调用 app 上的其他方法，或访问属性
    const token = jwt.sign({ userId, exp: Math.floor(Date.now() / 1000) + config.token.validTime }, config.jwt.secret);
    ctx.res.setHeader('token', token)
    // redis 存储 token 与用户数据匹配
    await redis.set(`${WANG_ADMIN_USER_TOKEN_KEY}_${userId}`, token, 'EX', config.token.validTime * 1000);
  },

  /**
   * 清除制定用户的token
   */
  async clearTokenWithUserId(id: string) {
    const ctx = this as Context
    const { redis  } = ctx.app as any
    const userId = id ? id : this.getUserId()
    // redis 存储 token 与用户数据匹配
    await redis.set(`${WANG_ADMIN_USER_TOKEN_KEY}_${userId}`, '', 'EX', 1);
  },

  /**
   * 自定义封装GET
   * @param {*} url 请求地址
   * @param {*} data 请求参数
   * @param {*} other 其他
   */
  $wGet(url, data = {}, other = {}) {
    const { curl } = (this as any) as Context
    return curl(url, {
      dataType: 'json',
      method: 'GET',
      data,
      ...other,
    });
  },

  /**
   * 自定义封装POST
   * @param {*} url 请求地址
   * @param {*} data 请求参数
   */
  $wPost(url, data = {}) {
    const { curl } = (this as any) as Context
    return curl(url, {
      dataType: 'json',
      method: 'POST',
      data,
    });
  },

  /**
   * 响应html文件
   * @param {*} fileName 文件名
   */
  sendHtmlFile(fileName) {
    const { response } = (this as any) as Context
    (response as any).sendHtmlFile(fileName);
  },

  /**
   * 发送正确数据
   * @param {*} data 要发送的数据
   * @param {*} msg 要传输的额外信息
   */
  sendSuccess(data = {}, msg = '') {
    const { response } = (this as any) as Context
    (response as any).sendSuccess(data, msg);
  },

  /**
   * 发送正确数据
   * @param {*} data 要发送的数据
   * @param {*} msg 要传输的额外信息
   */
  sendList(data) {
    const { query } = (this as any) as Context
    const { pageSize, pageNum } = query
    const resData = {
      list: data[0],
      total: data[1],
      pageSize,
      pageNum
    };
    this.sendSuccess(resData);
  },

  /**
   * 获取请求体
   */
  w_body(keys?: string | Array<string>) {
    const { request } = (this as any) as Context
    const { body } = request
    if (keys) {
      const newKeys = typeof keys === 'string' ? keys.split(' ') : [...keys]
      const json = {}
      newKeys.forEach((key) => {
        json[key] = body[key]
      })
      return json
    }
    return body
  },

  /**
   * 自定义工具类
   */
  w_tool: Tool,

  /**
   * 校验分页
   */
  w_get_cookie(c?: string) {
    const ctx = (this as any) as Context
    const cookie = c ? c : ctx.request.header.cookie
    const kvArr = cookie.split('; ')
    const obj = {}
    kvArr.forEach((item) => {
      const [key, val] = item.split('=')
      obj[key] = val
    })
    return obj
  },

  // ========================== 数据校验

  /**
   * 校验分页
   */
  w_validate_page() {
    const ctx = (this as any) as Context
    ctx.validate({
      pageSize: {
        required: false,
        type: 'number',
        convertType: 'number',
        default: 10,
        min: 1,
        max: 100,
      },
      pageNum: {
        required: false,
        convertType: 'number',
        type: 'number',
        default: 1,
        min: 1,
      },
    }, ctx.query);
  },
  w_validate_param_id() {
    const ctx = (this as any) as Context
    ctx.validate({
      id: {
        type: 'mongo-id',
      },
    }, ctx.params);
  },
  w_validate_param_uuid() {
    const ctx = (this as any) as Context
    ctx.validate({
      uuid: {
        type: 'uuid',
      },
    }, ctx.params);
  },
  /**
   * 校验参数ID
   */
  /**
   * 校验UUID
   */
  w_validate_uuid: {
    type: 'string',
    format: uuid,
    trim: true,
  },
  /**
   * 校验名称
   */
  w_validate_name: {
    type: 'string',
    format: /^[0-9A-Za-z]{4,20}$/,
    trim: true,
  },
  /**
   * 校验其他名称
   */
  w_validate_otherName: {
    type: 'string',
    min: 1,
    max: 20
  },
  /**
   * 校验密码
   */
  w_validate_pass: {
    type: 'string',
    format: /^[0-9A-Za-z]{6,20}$/,
    trim: true,
  },
  /**
   * 校验验证码
   */
  w_validate_code: {
    type: 'string',
    format: /^[0-9A-Za-z]{4}$/,
    trim: true,
  },
  /**
   * 校验描述
   */
  w_validate_description: {
    type: 'string',
    max: 140,
    required: false,
  },
  /**
   * 校验页面路径
   */
  w_validate_pagePath: {
    type: 'string',
    format: /\/:id$/,
    trim: true,
    min: 6,
    max: 100
  },
  /**
   * 获取数据
   * @param {*} key 缓存的键值
   * @param {*} getDataFunc 获取数据的方法，异步方法，如果没有缓存就会执行当前方法
   * @param {*} isLocal 是否本地直接缓存数据，如果是返回304必须是浏览器第一次请求过数据，每个人都会缓存自己的
   *                    如果是本地缓存。直接缓存数据，所有人请求都会获取缓存的数据
   */
  async getCache(key, getDataFunc, isLocal = false) {
    const { req, app } = this as Context
    const { redis } = app
    // 获取缓存的数据
    const serverCatchData = await redis.get(key);
    if (serverCatchData) { // 判断是否有缓存的数据
      if (isLocal) {  // 是否是本地直接缓存数据
        try {
          // 将数据反序列化拿到
          return JSON.parse(serverCatchData).data
        } catch {
          // 有可能获取的数据是有问题的，不能反序列化导致报错，直接删除掉当前redis存储的数据
          await redis.del(key);
        }
      } else {
        // 获取客户端时间
        const clientDate = req.headers['if-modified-since']
        // 如果客户端的时间等于缓存的时间。直接返回304状态，获取浏览器缓存的数据
        if (clientDate === serverCatchData) {
          (this as Context).throw(304)
        }
      }
    } 
    const resData = await getDataFunc()
    let catchData = ''
    if (isLocal) {
      catchData = JSON.stringify({
        data: resData
      })
    } else {
      // 首先判断redis是否缓存了时间，因为有可能是第1个人已经缓存了时间,
      // 那么第二个人进来时间应该是保持不变，如果改变缓存时间，即便数据不变，第一个人再次获取仍然不是缓存数据
      catchData = serverCatchData || (new Date() as any).toGMTString()
      // 设置请求头缓存
      (this as Context).set('Last-Modified', catchData)
      (this as Context).set('Cache-Control', 'no-cache')
    }
    // 如果304缓存并发现有已经有缓存的数据，就没必要你再次到redis缓存，因为相等
    if (isLocal || (!isLocal && !serverCatchData)) {
      await redis.set(key, catchData, 'EX', 1000 * 3600 * 24 * 7);
    }
    return resData
  },

  /**
   * 获取列表缓存数据
   * @param {*} key 缓存的键值
   * @param {*} getDataFunc 获取数据的方法，异步方法，如果没有缓存就会执行当前方法
   */
  async getListCache(key, getDataFunc) {
    const { req, app, query } = this as Context
    const { redis } = app
    // 获取请求参数序列化
    const queryKey = JSON.stringify({ ...query })
    // 获取客户端时间
    const clientDate = req.headers['if-modified-since']
    // 获取服务端缓存时间
    const serverCatchDate = await redis.hget(key, queryKey);
    // 如果服务端有数据，并且等于客户端带过来的时间，就是有缓存的数据
    if (serverCatchDate && clientDate === serverCatchDate) {
      (this as Context).throw(304)
    }
    // 没有缓存，获取数据
    const resData = await getDataFunc()
    // 缓存时间
    const dataTimeStr = serverCatchDate || (new Date() as any).toGMTString();
    (this as Context).set('Last-Modified', dataTimeStr);
    (this as Context).set('Cache-Control', 'no-cache')
    // 如果之前已经缓存了时间就没必要再次缓存
    if (!serverCatchDate) {
      await redis.hset(key, queryKey, dataTimeStr);
    }
    return resData
  },

  /**
   * 清除缓存
   * 添加、删除数据的时候需要清除掉列表缓存
   * 修改数据的时候需要清除掉列表缓存数据的跟详情缓存
   * @param {*} data 缓存的KEY
   */
  async clearCache(...data) {
    const { app } = this as Context
    const { redis } = app
    await redis.del(...data);
  },
};
